/*
 * RandomGenerators.java
 *
 * Created on 12 May 2006, 18:05
 *
 * Copyright (C)
 * This program is free software; you can redistribute it and/or modify it under 
 * the terms of the GNU General Public License as published by the Free Software 
 * Foundation; either version 2 of the License, or (at your option) any later 
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT 
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple 
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */

package com.grubby.utils;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class provides a bunch of methods to aid in random selection of things
 * such as numbers within a range and objects from an array of objects.
 *
 * It was originally copied from examples provided by Jonathan Vaughan of
 * Monks/Vaughan Ltd.
 *
 * @author A. HARMEL-LAW. E: andrew.harmel.law@gmail.com
 * @author J.S. Vaughan
 * @version 0.1
 */
public class RandomGenerators {
    
    public static final Logger LOGGER 
            = Logger.getLogger("com.grubby.utils.RandomGenerators");
    
    /** Map containing counters for incremental number generation */
    private static Map myCounters = new HashMap();
    
    /** Constant int representing base 10 */
    private static final int BASE_10 = 10;
    
    /** Constant int representing ASCII 'A' value */
    private static final int ASCII_A = 65;
    
    /** Constant int representing ASCII 'Z' value */
    private static final int ASCII_Z = 90;
    
    /**
     * Returns a random <Object> from the supplied array. The <Random> object 
     * will be seeded based on the current system time.
     * <P>
     * Nullsafe - NULL will be returned if the <Random> object passed as a 
     * parameter is null.
     * @param An array of <Object>s
     * @return One of the <Object>s, randomly chosen
     */
    public static Object getRandom(Object[] list) {
        
        // Check for null
        if (list == null) {
            LOGGER.log(Level.WARNING, "The <Object> array passed as an " +
                    "argument was null.  Returning NULL.");
            return null;
        }
        
        Random rand = new Random(System.currentTimeMillis());
        return list[rand.nextInt(list.length)];
        
    }
    
    /**
     * Returns a randomly selected <Object> from the supplied array.
     * <P>
     * Nullsafe - NULL will be returned if the <Random> object passed as a 
     * parameter is null.
     * @param An array of <Object>s
     * @param rand The seeded <Random> to use to select the random <Object>
     * @return One of the <Object>s, randomly chosen
     */
    public static Object getRandom(Object[] list, Random rand) {
        
        // Check for null
        if (list == null) {
            LOGGER.log(Level.WARNING, "The array object passed as an argument " +
                    "was null.  Returning NULL.");
            return null;
        }
        
        // Check for null
        if (rand == null) {
            LOGGER.log(Level.WARNING, "The random object passed as an argument " +
                    "was null.  Returning NULL.");
            return null;
        }
        
        return list[rand.nextInt(list.length)];
        
    }
    
    /**
     * Return a random int from between the supplied min and max. The <Random>
     * object will be seeded based on the current system time.
     * @param min The minimum value the number can have
     * @param max The maximum value the number can have
     * @return The random number
     */
    public static int getRandom(int min, int max) {
        
        Random rand = new Random(System.currentTimeMillis());
        int interval = max - min;
        return (int) (min + (interval * rand.nextDouble()));
        
    }
    
    /**
     * Return a random int from between the supplied min and max.
     * @param min The minimum value the number can have
     * @param max The maximum value the number can have
     * @param rand The seeded <Random> to use to select the random int
     * @return The number chosen randomly
     */
    public static int getRandom(int min, int max, Random rand) {
        
        int interval = max - min;
        return (int) (min + (interval * rand.nextDouble()));
        
    }
    
    /**
     * Return a random int from the supplied int array. The <Random>
     * object will be seeded based on the current system time.
     * @param aList The array of int's
     * @return The number chosen randomly
     */
    public static int getRandom(int[] aList) {
        
        Random rand = new Random(System.currentTimeMillis());
        return aList[rand.nextInt(aList.length)];
        
    }
    
    /**
     * Return a random int from the supplied int array.
     * @param aList The array of int's
     * @param rand The seeded <Random> to use to select the random int
     * @return The number chosen randomly
     */
    public static int getRandom(int[] aList, Random rand) {
        
        return aList[rand.nextInt(aList.length)];
        
    }
    
    /**
     * Return a random long from between the supplied min and max. The <Random>
     * object will be seeded based on the current system time.
     * @param min The minimum value the number can have
     * @param max The maximum value the number can have
     * @return The number chosen randomly
     */
    public static long getRandom(long min, long max) {
        
        Random rand = new Random(System.currentTimeMillis());
        long interval = max - min;
        return (long) (min + (interval * rand.nextDouble()));
        
    }
    
    /**
     * Return a random long from between the supplied min and max.
     * @param min The minimum value the number can have
     * @param max The maximum value the number can have
     * @param rand The seeded <Random> object to use to generate the random value
     * @return The number chosen randomly
     */
    public static long getRandom(long min, long max, Random rand) {
        
        long interval = max - min;
        return (long) (min + (interval * rand.nextDouble()));
        
    }
    
    /**
     * Return a random double from between the supplied min and max. The 
     * <Random> object will be seeded based on the current system time.
     * @param min The minimum value the number can have
     * @param max The maximum value the number can have
     * @return The number chosen randomly
     */
    public static double getRandom(double min, double max) {
        
        Random rand = new Random(System.currentTimeMillis());
        double interval = max - min;
        return min + (interval * rand.nextDouble());
        
    }
    
    /**
     * Return a random double from between the supplied min and max.
     * @param min The minimum value the number can have
     * @param max The maximum value the number can have
     * @param rand The seeded Random object to use to generate the random value
     * @return The number chosen randomly
     */
    public static double getRandom(double min, double max, Random rand) {
            
        double interval = max - min;
        return min + (interval * rand.nextDouble());
        
    }
    
    /**
     * Return a random Boolean.TRUE or Boolean.FALSE object based on the current
     * system time
     * @return The Boolean value
     */
    public static Boolean getRandomBoolean() {
        
        return Boolean.valueOf(System.currentTimeMillis() % 2 == 0);
        
    }
    
    /**
     * Return a random char from the provided String.
     * @param theString The <String> to select the char from.
     * @return The char selected.
     */
    public static char getRandomCharFromString(String theString) {
        
        Random rand = new Random(System.currentTimeMillis());
        
        return theString.charAt(rand.nextInt(theString.length()));
        
    }
    
    /**
     * Return a random char from the provided String.
     * @param theString The <String> to select the char from.
     * @param rand The seeded Random object to use to select the random char
     * @return The char selected.
     */
    public static char getRandomCharFromString(String theString, Random rand) {
        
        return theString.charAt(rand.nextInt(theString.length()));
        
    }
    
    /**
     * Return a random <String> from the provided array.
     * <P>
     * Nullsafe - NULL will be returned if the <Random> object passed as a 
     * parameter is null.
     * @param theStrings The array of <String> objects to choose from
     * @return The randomly selected <String>
     */
    public static String getRandomStringFromArray(String[] theStrings, Random rand) {
        
        // Check for null
        if (rand == null) {
            LOGGER.log(Level.WARNING, "The random object passed as an argument " +
                    "was null.  Returning NULL.");
            return null;
        }
        
        return theStrings[rand.nextInt(theStrings.length)];
        
    }
    
    /**
     * Return a random <String> from the provided array.
     * <P>
     * Nullsafe - NULL will be returned if the <Random> object passed as a 
     * parameter is null.
     * @param theStrings The array of <String> objects to choose from
     * @param rand The seeded Random object to use to select the random <String>
     * @return The randomly selected <String>
     */
    public static String getRandomStringFromArray(String[] theStrings) {
        
        // Check for null
        if (theStrings == null) {
            LOGGER.log(Level.WARNING, "The random object passed as an argument " +
                    "was null.  Returning NULL.");
            return null;
        }
        
        Random rand = new Random(System.currentTimeMillis());
        
        return theStrings[rand.nextInt(theStrings.length)];
        
    }
    
    /**
     * Method to return an incrementing numeric String (incremented since the 
     * last call to this method)
     * @param length Length of the string
     * @return The value as a String
     */
    private static String getNumericString(int length) {
        
        Integer myLastValue = (Integer) myCounters.get(new Integer(length));
        if (myLastValue == null) {
            myLastValue = new Integer(1);
        } else {
            int newValue = myLastValue.intValue() + 1;
            if (newValue == (length * BASE_10)) {
                newValue = 1;
            }
            myLastValue = new Integer(newValue);
        }
        myCounters.put(new Integer(length), myLastValue);
        String myUnpaddedValue = myLastValue.toString();
        
        return StringUtils.paddingString(myUnpaddedValue, length, '0', true);
        
    }
    
    /**
     * Generates an random alpha-numeric String, based on a provided pattern.
     * <P>
     * Nullsafe - NULL will be returned if the <Random> object passed as a 
     * parameter is null.
     * @param aPattern A pattern like AAAANNNNNAAAA returns a string where A's
     * are replaced with random alphanumeric characters and 'NNN' is a spread
     * out version of an incrementing number - guaranteeing uniqueness within
     * the range of that number (remember must have at least one N)
     * @return String the generated number String
     */
    public static String getStringFromPattern(String aPattern) {
        
        // Check for null
        if (aPattern == null) {
            LOGGER.log(Level.WARNING, "The <String> passed as an argument " +
                    "was null.  Returning NULL.");
            return null;
        }
        
        int myCountNumeric = 0;
        int myOverallLength = aPattern.length();
        for (int i = 0; i < myOverallLength; i++) {
            char c = aPattern.charAt(i);
            if (c == 'N') {
                myCountNumeric++;
            }
        }
        
        // Get a new integer of length myCountNumeric
        String myNumStr = getNumericString(myCountNumeric);
        StringBuffer myRetval = new StringBuffer();
        int myNumbersUsed = 0;
        for (int i = 0; i < myOverallLength; i++) {
            char c = aPattern.charAt(i);
            if (c == 'N') {
                myRetval.append(myNumStr.charAt(myNumbersUsed));
                myNumbersUsed++;
            } else {
                myRetval.append((char) getRandom(ASCII_A, ASCII_Z, null));
            }
        }
        
        return myRetval.toString();
        
    }
    
    /**
     * This class selects an option for an array of choices provided based on a 
     * percentage chance of selecting each.
     * <P>
     * If the weighting does not total 100 or the arrays are of different sizes 
     * NULL is returned.
     * <P>
     * Nullsafe - NULL will be returned if the <Random> object passed as a 
     * parameter is null.
     * @param weighting An array containing the relative weightings for each 
     * option
     * @param options An array containing the options to select from
     * @return The selected option
     */
    public static Object selectRandomPercentWeighted(int[] weighting, 
            Object[] options) {
        
        // Check for null
        if (options == null) {
            LOGGER.log(Level.WARNING, "The <Object> array passed as an argument " +
                    "was null.  Returning NULL.");
            return null;
        }
        
        // Check the arrays are the same length
        if (weighting.length != options.length) {
            LOGGER.log(Level.WARNING, "The arrays were of different lengths!  " +
                    "Returning NULL.");
            return null;
        }
        
        // CHECK THE WEIGHTINGS ADD UP TO 100%
        int total = 0;
        for (int i = 0 ; i < weighting.length ; i ++) {
            total += weighting[i];
        }
        // If not, return null
        if (total != 100){
            LOGGER.log(Level.WARNING, "The weightings provided do not add up to " +
                    "100!  Returning NULL.");
            return null;
        }
        
        // Get a random number between 1 and 100
        int randomSelection = getRandom(1, 100, null);
        
        // Find out which option this falls within
        int runningTotal = 0;
        int counter;
        for (counter = 0 ; counter < weighting.length ; counter ++) {
            runningTotal += weighting[counter];
            if (randomSelection <= runningTotal) {
                break;
            }
        }
        return options[counter];
        
    }
    
} // end of class RandomGenerators.java